package jsyntaxpane.util.intellisense.bsh;

import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jmine.tec.synthetic.Import;
import jmine.tec.synthetic.LocalVariable;
import jsyntaxpane.util.intellisense.AutocompleteTool;

/**
 * @author takeshi
 */
public class BshAutocompleteTool implements AutocompleteTool {

    private final Map<String, Class<?>> globalVariables = new ConcurrentHashMap<String, Class<?>>();

    private final List<Member> globalMembers = Collections.synchronizedList(new LinkedList<Member>());

    private final ImportsCalculator imports = new ImportsCalculator();

    public static final String RIGHT_DELIMITER = "[\\s;,=)]";

    // we miss special letters like unicode chars
    public static final String TYPE_IDENTIFIER = "[\\w$_][\\w$_.]*";

    // we miss special letters like unicode chars
    public static final String JAVA_IDENTIFIER = "[\\w$_]+";

    public static final String REGEX = "\\s*" + TYPE_IDENTIFIER + "\\s+(\\s*" + JAVA_IDENTIFIER + "[(].*?[)])[\\s{]";

    public static final Pattern VARIABLE_PATTERN = Pattern.compile(TYPE_IDENTIFIER + "\\s+(" + JAVA_IDENTIFIER + ")\\s*=");

    public static final Pattern IMPORT_STATEMENT_PATTERN = Pattern.compile("\\s+import\\s+([\\w$_.*]+);");

    public static final Pattern EXTRACT_FUNCTION_SIGNATURE = Pattern.compile(REGEX);

    /**
     * 
     */
    public BshAutocompleteTool() {
        addImport("java.lang.*");
    }

    /**
     * {@inheritDoc}
     */
    public void addGlobalMethod(String signature, Class<?> hostType) {
        globalMembers.add(new NamedSyntheticMember(signature, hostType));
    }

    /**
     * {@inheritDoc}
     */
    public void addGlobalVariable(Class<?> variableType, String variable, Class<?> hostType) {
        this.globalVariables.put(variable, variableType);
        this.globalMembers.add(new NamedSyntheticMember(variable, hostType));
    }

    /**
     * {@inheritDoc}
     */
    public void addImport(String imp) {
        this.imports.addImport(imp);
    }

    /**
     * @param typeToken the type token
     * @param defType the default type
     * @return Class<?>
     */
    public Class<?> resolveTypeToken(String typeToken, Class<?> defType) {
        return imports.resolveTypeToken(typeToken, defType);
    }

    /**
     * Tenta achar o tipo de uma variavel
     * 
     * @param validText String
     * @param variable String
     * @param localImports the {@link ImportsCalculator}
     * @return {@link Class} defaults to Object
     */
    public Class<?> resolveVariableType(String validText, String variable, ImportsCalculator localImports) {
        Class<?> fixed = this.globalVariables.get(variable);
        if (fixed != null) {
            return fixed;
        }
        Pattern pat = getVariableDeclarationPattern(variable);
        MatchResult match = null;
        Matcher matcher = pat.matcher(validText);
        while (matcher.find()) {
            match = matcher.toMatchResult();
        }
        if (match == null) {
            // cannot find variable declaration
            return Object.class;
        }
        return localImports.resolveTypeToken(match.group(1), Object.class);
    }

    /**
     * Devolve uma {@link Pattern} que procura declaracoes de variavel com o nome passado
     * 
     * @param variable {@link String}
     * @return {@link Pattern}
     */
    public static Pattern getVariableDeclarationPattern(String variable) {
        return Pattern.compile("(" + TYPE_IDENTIFIER + "+)\\s*+" + variable + RIGHT_DELIMITER);
    }

    /**
     * Devolve o escopo valido ateh a posicao passada, ignorando strings e blocos entre { }
     * 
     * @param text String
     * @param position int
     * @return {@link String}
     */
    private String selectLastVisibleScope(String text, int position) {
        return new ScopeParser(text).getValidScopeUpToExcludeString(position, " ");
    }

    /**
     * {@inheritDoc}
     */
    public List<Member> getCompletionFor(String text, int position) {
        String token = getCurrentToken(text, position);
        int separator = position - token.length() - 1;
        if (separator >= 0) {
            char sep = text.charAt(separator);
            if (sep == '.') {
                return getCompletionsForMethodCall(text, separator);
            }
        }
        return getFreeCompletions(text, position);
    }

    /**
     * Devolve a lista de {@link Member} que devem ser inseridas livres de contexto - por exemplo, variaveis globais
     * 
     * @param text {@link String}
     * @param position int
     * @return {@link List} de {@link Member}
     */
    private List<Member> getFreeCompletions(String text, int position) {
        List<Member> list = new ArrayList<Member>(findLocals(text, position));
        for (Member method : this.globalMembers) {
            list.add(method);
        }
        return list;
    }

    /**
     * @param text {@link String}
     * @param position int
     * @return {@link List} de {@link String}
     */
    private List<Member> findLocals(String text, int position) {
        String select = selectLastVisibleScope(text, position);
        List<Member> list = new ArrayList<Member>();
        Matcher matcher = VARIABLE_PATTERN.matcher(select);

        while (matcher.find()) {
            list.add(new NamedSyntheticMember(matcher.group(1), LocalVariable.class));
        }
        ImportsCalculator calculator = this.getLocalImport(select);
        Collection<String> names = calculator.getImportedNames();
        for (String string : names) {
            list.add(new NamedSyntheticMember(string, Import.class));
        }
        return list;
    }

    /**
     * Devolve a lista de {@link Member} referentes a uma chamada a metodo ou campo
     * 
     * @param text {@link String}
     * @param dotPosition int
     * @return {@link List} de {@link Member}
     */
    private List<Member> getCompletionsForMethodCall(String text, int dotPosition) {
        String variable = extractMethodTarget(text, dotPosition);
        String lastVisibleScope = selectLastVisibleScope(text, dotPosition);
        ImportsCalculator local = getLocalImport(lastVisibleScope);
        if (local.resolveTypeToken(variable, null) != null) {
            Class<?> type = local.resolveTypeToken(variable, Object.class);
            return getStaticCompletionsForType(type);
        }
        return getCompletionsForType(resolveVariableType(lastVisibleScope, variable, local));
    }

    /**
     * Devolve um local import
     * 
     * @param lastVisibleScope String
     * @return {@link ImportsCalculator}
     */
    private ImportsCalculator getLocalImport(String lastVisibleScope) {
        ImportsCalculator local = new ImportsCalculator(this.imports);
        Matcher matcher = IMPORT_STATEMENT_PATTERN.matcher(lastVisibleScope);
        while (matcher.find()) {
            local.addImport(matcher.group(1));
        }
        return local;
    }

    /**
     * Devolve os {@link Member} estaticos do tipo passado
     * 
     * @param type {@link Class}
     * @return {@link List} de {@link Member}
     */
    private List<Member> getStaticCompletionsForType(Class<?> type) {
        List<Member> list = new ArrayList<Member>();
        for (Field f : type.getFields()) {
            if (Modifier.isStatic(f.getModifiers())) {
                list.add(f);
            }
        }
        for (Method m : type.getMethods()) {
            if (Modifier.isStatic(m.getModifiers())) {
                list.add(m);
            }
        }
        return list;
    }

    /**
     * @param type {@link Class}
     * @return {@link List} de {@link Member}
     */
    private List<Member> getCompletionsForType(Class<?> type) {
        List<Member> list = new ArrayList<Member>();
        for (Field f : type.getFields()) {
            list.add(f);
        }
        for (Method m : type.getMethods()) {
            list.add(m);
        }
        return list;
    }

    /**
     * Procura o alvo de uma invocacao de metodo
     * 
     * @param text o texto
     * @param limit p limite
     * @return {@link String}
     */
    private String extractMethodTarget(String text, int limit) {
        return getCurrentToken(text, limit);
    }

    /**
     * {@inheritDoc}
     */
    public String getCurrentToken(String text, int caret) {
        int pos = 0;
        for (int i = caret - 1; i >= 0; i--) {
            char c = text.charAt(i);
            if (!Character.isJavaIdentifierPart(c)) {
                pos = i + 1;
                break;
            }
        }
        return text.substring(pos, caret).trim();
    }

    public static final Comparator<Member> MEMBER_NAME_COMPARATOR = new Comparator<Member>() {

        public int compare(Member o1, Member o2) {
            return o1.getName().compareTo(o2.getName());
        }
    };

    /**
     * {@inheritDoc}
     */
    public void registerFixedType(String variable, Class<?> type) {
        this.globalVariables.put(variable, type);
    }

    /**
     * {@inheritDoc}
     */
    public void loadInitialization(String text, Class<?> host) {
        Matcher matcher = IMPORT_STATEMENT_PATTERN.matcher(text);
        while (matcher.find()) {
            this.addImport(matcher.group(1));
        }
        matcher = EXTRACT_FUNCTION_SIGNATURE.matcher(text);
        while (matcher.find()) {
            this.addGlobalMethod(matcher.group(1), host);
        }
    }

}
